using System;
using System.Collections;
using System.Reflection;
using System.Reflection.Emit;

namespace NUtil.beans
{
    public class ClassGenerator
    {
        public const string INVOCATION_HANDLER_FIELD_NAME = "_invocationHandler";

        internal const System.Reflection.BindingFlags ALL_INSTANCE_METHODS
            = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;

        readonly protected Type type;
        readonly protected IInvocationHandler handler;
        readonly protected IList methodsToIgnore;
        readonly protected Type superclassIfTypeIsInterface;

        public ClassGenerator(Type type, IInvocationHandler handler)
            : this(type, handler, new ArrayList()) { }

        public ClassGenerator(Type type, IInvocationHandler handler, IList methodsToIgnore)
            : this(type, handler, methodsToIgnore, null) { }

        public ClassGenerator(Type type, IInvocationHandler handler, IList methodsToIgnore, Type superclassIfTypeIsInterface)
        {
            this.type = type;
            this.handler = handler;
            this.methodsToIgnore = methodsToIgnore;
            this.superclassIfTypeIsInterface = superclassIfTypeIsInterface;
        }


        public virtual object Generate()
        {
            methodsToIgnore.Add("Equals");
            methodsToIgnore.Add("ToString");

            TypeBuilder typeBuilder = CreateTypeBuilder();
            MethodImplementor methodImplementor =
                new MethodImplementor(typeBuilder, DefineInvocationHandlerField(typeBuilder));

            ImplementMethods(methodImplementor);
            return CreateProxyInstance(typeBuilder.CreateType());
        }

        TypeBuilder CreateTypeBuilder()
        {
            AppDomain appDomain = AppDomain.CurrentDomain;
            AssemblyName assemblyName = new AssemblyName();
            assemblyName.Name = "DynamicProxyAssembly";
            AssemblyBuilder assemblyBuilder = appDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
            ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule("MockModule");
            return moduleBuilder.DefineType(ProxyClassName, TypeAttributes.Public, ProxySuperClass, ProxyInterfaces);
        }

        private FieldBuilder DefineInvocationHandlerField(TypeBuilder typeBuilder)
        {
            return typeBuilder.DefineField(
                ClassGenerator.INVOCATION_HANDLER_FIELD_NAME,
                typeof(IInvocationHandler), FieldAttributes.Public);
        }

        private void ImplementMethods(MethodImplementor methodImplementor)
        {
            foreach (Type currentType in new InterfaceLister().List(type))
            {
                foreach (MethodInfo methodInfo in currentType.GetMethods(ALL_INSTANCE_METHODS))
                {
                    if (ShouldImplement(methodInfo))
                    {
                        methodImplementor.Implement(methodInfo);
                    }
                }
            }
        }

        private bool ShouldImplement(MethodInfo methodInfo)
        {
            if ((!methodInfo.IsVirtual) || methodInfo.IsFinal || methodInfo.IsAssembly)
            {
                methodsToIgnore.Add(methodInfo.Name);
            }
            return !methodsToIgnore.Contains(methodInfo.Name);
        }

        private object CreateProxyInstance(Type proxyType)
        {
            object result = Activator.CreateInstance(proxyType);


            FieldInfo handlerField = proxyType.GetField(INVOCATION_HANDLER_FIELD_NAME);
            handlerField.SetValue(result, handler);

            return result;
        }

        public string ProxyClassName { get { return "Proxy" + type.Name; } }
        public Type ProxySuperClass { get { return type.IsInterface ? superclassIfTypeIsInterface : type; } }
        public Type[] ProxyInterfaces { get { return type.IsInterface ? new Type[] { type } : new Type[0]; } }

    }

    public class MethodImplementor
    {
        private BoxingOpCodes boxingOpCodes = new BoxingOpCodes();
        private TypeBuilder typeBuilder;
        private FieldBuilder handlerFieldBuilder;

        public MethodImplementor(TypeBuilder aTypeBuilder, FieldBuilder handlerFieldBuilder)
        {
            typeBuilder = aTypeBuilder;
            this.handlerFieldBuilder = handlerFieldBuilder;
        }

        public virtual void Implement(MethodInfo methodInfo)
        {
            Type returnType = methodInfo.ReturnType;
            ParameterInfo[] parameterInfo = methodInfo.GetParameters();
            Type[] paramTypes = ExtractParameterTypes(parameterInfo);
            ILGenerator il = CreateILGenerator(methodInfo, returnType, paramTypes, parameterInfo);

            EmitMethodSignature(methodInfo, parameterInfo, il);
            EmitMethodCall(il);

            EmitOutParams(parameterInfo, il);

            EmitMethodReturn(returnType, il);
        }

        public virtual void EmitOutParams(ParameterInfo[] parameterInfos, ILGenerator il)
        {
            for (int i = 0; i < parameterInfos.Length; i++)
            {
                //				il.Emit(OpCodes.Ldarg, i+1);
            }
        }

        private Type[] ExtractParameterTypes(ParameterInfo[] parameters)
        {
            Type[] paramTypes = new Type[parameters.Length];
            for (int i = 0; i < parameters.Length; ++i)
            {
                paramTypes[i] = parameters[i].ParameterType;
            }
            return paramTypes;
        }

        private ILGenerator CreateILGenerator(MethodInfo methodInfo, Type returnType, Type[] paramTypes, ParameterInfo[] paramInfos)
        {
            MethodBuilder methodBuilder =
                typeBuilder.DefineMethod(methodInfo.Name, MethodAttributes.Public | MethodAttributes.Virtual, returnType, paramTypes);

            //			for (int i = 0; i < paramInfos.Length; i++) 
            //			{
            //				if (paramInfos[i].IsOut) 
            //				{
            //					methodBuilder.DefineParameter(i, ParameterAttributes.Out, paramInfos[i].Name);
            //				}
            //			}			

            return methodBuilder.GetILGenerator();
        }

        private void EmitMethodSignature(MethodInfo methodInfo, ParameterInfo[] paramTypes, ILGenerator il)
        {
            il.DeclareLocal(typeof(object));
            il.Emit(OpCodes.Ldarg_0);
            il.Emit(OpCodes.Ldfld, handlerFieldBuilder);
            il.Emit(OpCodes.Ldstr, StripGetSetPrefix(methodInfo));
            il.Emit(OpCodes.Ldc_I4_S, paramTypes.Length);
            il.Emit(OpCodes.Newarr, typeof(object));

            if (paramTypes.Length > 0)
            {
                il.Emit(OpCodes.Stloc_0);
                il.Emit(OpCodes.Ldloc_0);

                for (int i = 0; i < paramTypes.Length; i++)
                {

                    il.Emit(OpCodes.Ldc_I4_S, i);
                    il.Emit(OpCodes.Ldarg_S, i + 1);
                    if (paramTypes[i].ParameterType.IsValueType)
                    {
                        il.Emit(OpCodes.Box, paramTypes[i].ParameterType);
                    }
                    il.Emit(OpCodes.Stelem_Ref);
                    il.Emit(OpCodes.Ldloc_0);
                }
            }
        }

        private void EmitMethodCall(ILGenerator il)
        {
            MethodInfo call = typeof(IInvocationHandler).GetMethod("Invoke");
            il.EmitCall(OpCodes.Callvirt, call, null);
        }

        private void EmitMethodReturn(Type returnType, ILGenerator il)
        {
            if (returnType == typeof(void))
            {
                il.Emit(OpCodes.Pop);
            }
            else
            {
                if (returnType.IsPrimitive || returnType.IsEnum)
                {
                    il.Emit(OpCodes.Unbox, returnType);
                    il.Emit(boxingOpCodes[returnType]);
                }
                else if (returnType.IsValueType)
                {
                    il.Emit(OpCodes.Unbox, returnType);
                    il.Emit(OpCodes.Ldobj, returnType);
                }

                il.DeclareLocal(returnType);
                il.Emit(OpCodes.Stloc_1);
                Label l = il.DefineLabel();
                il.Emit(OpCodes.Br_S, l);
                il.MarkLabel(l);
                il.Emit(OpCodes.Ldloc_1);
            }
            il.Emit(OpCodes.Ret);
        }

        private string StripGetSetPrefix(MethodInfo methodInfo)
        {
            string methodName;
            methodName = methodInfo.Name;
            if (methodName.StartsWith("get_") || methodName.StartsWith("set_"))
            {
                methodName = methodName.Substring(4);
            }
            return methodName;
        }

    }

    public class BoxingOpCodes
    {
        private static IDictionary boxingOpCodes;

        public OpCode this[Type aType]
        {
            get
            {
                return GetOpCode(aType);
            }
        }

        private static OpCode GetOpCode(Type aType)
        {
            if (boxingOpCodes == null)
            {
                boxingOpCodes = new Hashtable();
                boxingOpCodes[typeof(sbyte)] = OpCodes.Ldind_I1;
                boxingOpCodes[typeof(short)] = OpCodes.Ldind_I2;
                boxingOpCodes[typeof(int)] = OpCodes.Ldind_I4;
                boxingOpCodes[typeof(long)] = OpCodes.Ldind_I8;
                boxingOpCodes[typeof(byte)] = OpCodes.Ldind_U1;
                boxingOpCodes[typeof(ushort)] = OpCodes.Ldind_U2;
                boxingOpCodes[typeof(uint)] = OpCodes.Ldind_U4;
                boxingOpCodes[typeof(ulong)] = OpCodes.Ldind_I8;
                boxingOpCodes[typeof(float)] = OpCodes.Ldind_R4;
                boxingOpCodes[typeof(double)] = OpCodes.Ldind_R8;
                boxingOpCodes[typeof(char)] = OpCodes.Ldind_U2;
                boxingOpCodes[typeof(bool)] = OpCodes.Ldind_I1;
            }
            if (boxingOpCodes.Contains(aType))
            {
                return (OpCode)boxingOpCodes[aType];
            }
            else
            {
                return OpCodes.Ldind_I1;
            }
        }
    }
}
